package lc.tool;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.SortedMap;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.logging.Logger;

import lc.animation.Keyframe;
import lc.util.CoincidentTextureLocator;
import lc.util.Material;
import lc.util.Mesh;
import lc.util.SimpleFloatArray;
import lc.util.SimpleIntArray;
import lc.util.TextureMap;
import lc.util.Transform;

import com.jme.math.Matrix3f;
import com.jme.math.Quaternion;
import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import com.jme.scene.Node;
import com.jme.system.dummy.DummyDisplaySystem;
import com.jme.util.export.binary.BinaryExporter;
import com.jme.util.resource.ResourceLocatorTool;

public final class JmeFromAse {

	public final static Logger logger = Logger.getLogger(JmeFromAse.class.getName());

	/**
	 * @param args
	 */
	public final static void main(String[] args) {
		if (args.length != 2) {
			System.err.print("Usage: JmeFromAse input.ase output.jme\n");
			System.exit(1);
		}
		//	set some commonly used parameters
		JmeFromAse.UNIT_SCALE = JmeFromAse.INCHES_TO_METERS;
		JmeFromAse.Y_UP = true;
		CoincidentTextureLocator ctl = new CoincidentTextureLocator();
		ctl.basePath = args[1].substring(0, args[1].lastIndexOf('/'));
		ResourceLocatorTool.addResourceLocator(ResourceLocatorTool.TYPE_TEXTURE, ctl);
		new DummyDisplaySystem();
		JmeFromAse conv = new JmeFromAse();
		File src = new File(args[0]);
		if (!src.canRead()) {
			System.err.print("Input file " + args[0] + " does not exist or is not readable.\n");
			System.exit(1);
		}
		File dst = new File(args[1]);
		if (dst.exists() && !dst.canWrite()) {
			System.err.print("Input file " + args[1] + " is not writable.\n");
			System.exit(1);
		}
		try {
			conv.convert(src, dst);
		}
		catch (Exception x) {
			System.err.print("Exception when converting " + args[0] + " to " + args[1] + "\n");
			System.err.print(x.getMessage() + "\n");
			x.printStackTrace();
			System.exit(2);
		}
	}

	/**
	 * UNIT_SCALE allows you to re-scale the units of an imported scene. For example,
	 * to go from inches to meters, try 0.0254.
	 */
	public static float UNIT_SCALE = 1.0f;
	/**
	 * Assign UNIT_SCALE the value of INCHES_TO_METERS to convert from inches to meters
	 * on import (shrinking the geometry).
	 */
	public final static float INCHES_TO_METERS = 0.0254f;
	/**
	 * Assign UNIT_SCALE the value of FEET_TO_METERS to convert from feet to meters
	 * on import (shrinking the geometry).
	 */
	public final static float FEET_TO_METERS = 0.3048f;
	/**
	 * Set Y_UP to true if you want to add a rotation that makes "Y" the up axis.
	 */
	public static boolean Y_UP = false;
	/**
	 * Set MIRROR_Z to true if you want to import geometry as left-handed.
	 * (It is generally converted to right-handed unless this is set).
	 * If you think you want to set this to true, chances are you're wrong.
	 */
	public static boolean MIRROR_Z = false;

	public final void convert(File inFile, File outFile) throws Exception {
		String name = inFile.getName().substring(inFile.getName().lastIndexOf('/')+1);
		int dotIndex = name.lastIndexOf('.');
		if (dotIndex > 0) {
			name = name.substring(0, dotIndex);
		}
		FileInputStream fis = new FileInputStream(inFile);
		FileOutputStream fos = new FileOutputStream(outFile);
		convert(fis, fos, name);
		fis.close();
		fos.close();
	}

	ArrayList<Mesh> meshes;
	ArrayList<Material> materials;
	Node rootNode;
	String contents;
	Hashtable<String, Node> nodesByName;
	boolean hasAnimation;
	float framespeed = 30;
	float ticksPerFrame = 160;
	float secsPerTick = 1.0f / (framespeed * ticksPerFrame);
	int lastframe = 29;

	public final void convert(InputStream src, OutputStream dst, String name) throws Exception {
		meshes = new ArrayList<Mesh>();
		hasAnimation = false;
		materials = new ArrayList<Material>();
		rootNode = new Node(name);
		nodesByName = new Hashtable<String, Node>();
		byte[] backing = new byte[src.available()];
		src.read(backing);
		contents = new String(backing);
		StringTokenizer tok = new StringTokenizer(contents);
		scanTopLevel(tok);
		if (Y_UP) {
			Quaternion q = new Quaternion();
			q.fromAngles(-(float)(Math.PI/2), 0, 0);
			rootNode.setLocalRotation(q);
		}
		if (hasAnimation) {
			Mesh.bakeAnimations(rootNode, (lastframe + 1) / framespeed);
		}
		BinaryExporter.getInstance().save(rootNode, dst);
	}

	public String token = "";

	static final String OPEN_BRACE = "{";
	static final String MATERIAL_LIST = "*MATERIAL_LIST";
	static final String GEOMOBJECT = "*GEOMOBJECT";
	static final String HELPEROBJECT = "*HELPEROBJECT";
	static final String MATERIAL = "*MATERIAL";
	static final String SUBMATERIAL = "*SUBMATERIAL";
	static final String MATERIAL_NAME = "*MATERIAL_NAME";
	static final String MATERIAL_AMBIENT = "*MATERIAL_AMBIENT";
	static final String MATERIAL_DIFFUSE = "*MATERIAL_DIFFUSE";
	static final String MATERIAL_SPECULAR = "*MATERIAL_SPECULAR";
	static final String MATERIAL_SHINE = "*MATERIAL_SHINE";
	static final String MAP_DIFFUSE = "*MAP_DIFFUSE";
	static final String MAP_OPACITY = "*MAP_OPACITY";
	static final String BITMAP = "*BITMAP";
	static final String UVW_U_OFFSET = "*UVW_U_OFFSET";
	static final String UVW_V_OFFSET = "*UVW_V_OFFSET";
	static final String UVW_U_TILING = "*UVW_U_TILING";
	static final String UVW_V_TILING = "*UVW_V_TILING";
	static final String NODE_NAME = "*NODE_NAME";
	static final String NODE_PARENT = "*NODE_PARENT";
	static final String NODE_TM = "*NODE_TM";
	static final String TM_ROW0 = "*TM_ROW0";
	static final String TM_ROW1 = "*TM_ROW1";
	static final String TM_ROW2 = "*TM_ROW2";
	static final String TM_ROW3 = "*TM_ROW3";
	static final String TIMEVALUE = "*TIMEVALUE";
	static final String MESH = "*MESH";
	static final String MESH_VERTEX_LIST = "*MESH_VERTEX_LIST";
	static final String MESH_VERTEX = "*MESH_VERTEX";
	static final String MESH_FACE_LIST = "*MESH_FACE_LIST";
	static final String MESH_FACE = "*MESH_FACE";
	static final String MESH_TVERTLIST = "*MESH_TVERTLIST";
	static final String MESH_TVERT = "*MESH_TVERT";
	static final String MESH_TFACELIST = "*MESH_TFACELIST";
	static final String MESH_TFACE = "*MESH_TFACE";
	static final String MESH_NORMALS = "*MESH_NORMALS";
	static final String MESH_FACENORMAL = "*MESH_FACENORMAL";
	static final String MESH_VERTEXNORMAL = "*MESH_VERTEXNORMAL";
	static final String MATERIAL_REF = "*MATERIAL_REF";
	static final String MESH_MTLID = "*MESH_MTLID";
	static final String MESH_SMOOTHING = "*MESH_SMOOTHING";
	static final String WIREFRAME_COLOR = "*WIREFRAME_COLOR";
	static final String TM_ANIMATION = "*TM_ANIMATION";
	static final String CONTROL_POS_TRACK = "*CONTROL_POS_TRACK";
	static final String CONTROL_POS_SAMPLE = "*CONTROL_POS_SAMPLE";
	static final String CONTROL_ROT_TRACK = "*CONTROL_ROT_TRACK";
	static final String CONTROL_ROT_SAMPLE = "*CONTROL_ROT_SAMPLE";
	static final String SCENE_LASTFRAME = "*SCENE_LASTFRAME";
	static final String SCENE_FRAMESPEED = "*SCENE_FRAMESPEED";
	static final String SCENE_TICKSPERFRAME = "*SCENE_TICKSPERFRAME";
	static final String SCENE = "*SCENE";

	final void scanTopLevel(StringTokenizer tok) throws Exception {
		while (nextToken(tok) != null) {
			if (token.charAt(0) == '*') {
				if (token.equals(MATERIAL_LIST)) {
					scanMaterialList(tok);
				}
				else if (token.equals(GEOMOBJECT)) {
					scanGeomObject(tok);
				}
				else if (token.equals(HELPEROBJECT)) {
					scanGeomObject(tok);
				}
				else if (token.equals(SCENE)) {
					scanScene(tok);
				}
			}
			else if (token.charAt(0) == '{') {
				//	this just skips blocks I don't understand
				countUpDown(tok);
			}
		}
	}

	final void scanScene(StringTokenizer tok) throws Exception {
		nextOpenBrace(tok);
		while (nextToken(tok) != null) {
			if (token.charAt(0) == '}') {
				secsPerTick = 1.0f / (framespeed * ticksPerFrame);
				return;
			}
			else if (token.charAt(0) == '{') {
				countUpDown(tok);
			}
			else if (token.equals(SCENE_LASTFRAME)) {
				lastframe = nextInt(tok);
			}
			else if (token.equals(SCENE_FRAMESPEED)) {
				framespeed = nextFloat(tok);
			}
			else if (token.equals(SCENE_TICKSPERFRAME)) {
				ticksPerFrame = nextFloat(tok);
			}
		}
	}

	final void scanMaterialList(StringTokenizer tok) throws Exception {
		nextOpenBrace(tok);
		while (nextToken(tok) != null) {
			if (token.charAt(0) == '*') {
				if (token.equals(MATERIAL)) {
					scanMaterial(tok);
				}
			}
			else if (token.charAt(0) == '{') {
				//	this just skips blocks I don't understand
				countUpDown(tok);
			}
			else if (token.charAt(0) == '}') {
				return;
			}
		}
	}

	final void scanMaterial(StringTokenizer tok) throws Exception {
		Material mtl = new Material();
		mtl.name = "mtl";
		scanSubmaterial(tok, mtl);
		materials.add(mtl);
	}

	final void scanSubmaterial(StringTokenizer tok, Material mtl) throws Exception {
		mtl.id = nextInt(tok);
		nextOpenBrace(tok);
		while (nextToken(tok) != null) {
			if (token.charAt(0) == '*') {
				if (token.equals(SUBMATERIAL)) {
					Material sub = new Material();
					sub.name = "mtl";
					scanSubmaterial(tok, sub);
					if (mtl.subMaterials == null)
						mtl.subMaterials = new ArrayList<Material>();
					mtl.subMaterials.add(sub);
				}
				else if (token.equals(MATERIAL_NAME)) {
					mtl.name = nextString(tok);
				}
				else if (token.equals(MATERIAL_AMBIENT)) {
					mtl.addProperty(Material.AMBIENT, null, nextColor(tok));
				}
				else if (token.equals(MATERIAL_DIFFUSE)) {
					mtl.addProperty(Material.DIFFUSE, null, nextColor(tok));
				}
				else if (token.equals(MATERIAL_SPECULAR)) {
					mtl.addProperty(Material.SPECULAR, null, nextColor(tok));
				}
				else if (token.equals(MATERIAL_SHINE)) {
					mtl.addProperty(Material.POWER, null, nextFloat(tok) * 100);
				}
				else if (token.equals(MAP_DIFFUSE)) {
					TextureMap diffuseMap = new TextureMap();
					scanMap(tok, diffuseMap);
					mtl.addProperty(Material.DIFFUSE, null, diffuseMap);
				}
				else if (token.equals(MAP_OPACITY)) {
					TextureMap opacityMap = new TextureMap();
					scanMap(tok, opacityMap);
					mtl.addProperty(Material.OPACITY, null, opacityMap);
				}
			}
			else if (token.charAt(0) == '{') {
				//	this just skips blocks I don't understand
				countUpDown(tok);
			}
			else if (token.charAt(0) == '}') {
				return;
			}
		}
	}

	final void scanMap(StringTokenizer tok, TextureMap map) throws Exception {
		nextOpenBrace(tok);
		while (nextToken(tok) != null) {
			if (token.equals(BITMAP)) {
				map.path = nextString(tok).replace('\\', '/');
			}
			else if (token.equals(UVW_U_OFFSET)) {
				//	I really ought to pay attention to these...
				map.uOffset = nextFloat(tok);
			}
			else if (token.equals(UVW_V_OFFSET)) {
				map.vOffset = nextFloat(tok);
			}
			else if (token.equals(UVW_U_TILING)) {
				map.uTiling = nextFloat(tok);
			}
			else if (token.equals(UVW_V_TILING)) {
				map.vTiling = nextFloat(tok);
			}
			else if (token.charAt(0) == '}') {
				return;
			}
		}
	}

	final void scanGeomObject(StringTokenizer tok) throws Exception {
		nextOpenBrace(tok);
		Node ret = new Node();
		rootNode.attachChild(ret);
		Mesh mesh = null;
		while (nextToken(tok) != null) {
			if (token.equals(NODE_NAME)) {
				ret.setName(nextString(tok));
			}
			else if (token.equals(NODE_PARENT)) {
				String pName = nextString(tok);
				Node parent = nodesByName.get(pName);
				if (parent == null)
					parent = rootNode;
				parent.attachChild(ret);
			}
			else if (token.equals(NODE_TM)) {
				scanNodeTm(tok, ret);
			}
			else if (token.equals(MESH)) {
				assert(mesh == null);
				mesh = new Mesh(ret.getName());
				scanMesh(tok, mesh);
			}
			else if (token.charAt(0) == '{') {
				countUpDown(tok);
			}
			else if (token.charAt(0) == '}') {
				if (mesh != null) {
					mesh.bake(ret);
				}
				nodesByName.put(ret.getName(), ret);
				return;
			}
			else if (token.equals(MATERIAL_REF)) {
				if (mesh != null) {
					mesh.material = materials.get(nextInt(tok));
				}
			}
			else if (token.equals(WIREFRAME_COLOR)) {
				if (mesh != null && mesh.material == null) {
					//	custom material just for you!
					mesh.material = new Material();
					ColorRGBA c = nextColor(tok);
					mesh.material.addProperty(Material.AMBIENT, null, c);
					mesh.material.addProperty(Material.DIFFUSE, null, c);
					mesh.material.addProperty(Material.SPECULAR, null, new ColorRGBA(0.5f, 0.5f, 0.5f, 1.0f));
					mesh.material.addProperty(Material.POWER, null, 30.0f);
					mesh.material.name = String.format("#%2x%2x%2x%2x",
							(byte)(c.a * 255), (byte)(c.r * 255), (byte)(c.g * 255), (byte)(c.b * 255));
				}
			}
			else if (token.equals(TM_ANIMATION)) {
				scanAnimation(tok, ret);
			}
		}
	}

	final static void fixXlation(Matrix3f rotScale, Vector3f xlation) {
		//	If we don't want a mirrored mesh, fix up the values (because
		//	the file comes in mirrored).
		if (MIRROR_Z) {
			xlation.y *= -1;
			rotScale.m01 *= -1;
			rotScale.m21 *= -1;
			rotScale.m10 *= -1;
			rotScale.m12 *= -1;
		}
	}

	final static void fixKeyframes(SortedMap<Float, Keyframe> keyframes) {
		if (!MIRROR_Z)
			return;
		for (Keyframe k : keyframes.values()) {
			if (k.rotation != null) {
				Matrix3f rot = k.rotation.toRotationMatrix();
				rot.m01 *= -1;
				rot.m21 *= -1;
				rot.m10 *= -1;
				rot.m12 *= -1;
				k.rotation.fromRotationMatrix(rot);
			}
			if (k.translation != null)
				k.translation.y *= -1;
		}
	}

	final void scanAnimation(StringTokenizer tok, Node node) throws Exception {
		hasAnimation = true;
		nextOpenBrace(tok);
		TreeMap<Float, Keyframe> keyframes = new TreeMap<Float, Keyframe>();
		while (nextToken(tok) != null) {
			if (token.equals(CONTROL_POS_TRACK)) {
				scanPosTrack(tok, keyframes);
			}
			else if (token.equals(CONTROL_ROT_TRACK)) {
				scanRotTrack(tok, keyframes);
			}
			else if (token.charAt(0) == '{') {
				countUpDown(tok);
			}
			else if (token.charAt(0) == '}') {
				fixKeyframes(keyframes);
				Mesh.applyAnimation(keyframes, node, (lastframe + 1) / framespeed);
				return;
			}
		}
	}

	final void scanPosTrack(StringTokenizer tok, SortedMap<Float, Keyframe> keyframes) throws Exception {
		nextOpenBrace(tok);
		while (nextToken(tok) != null) {
			if (token.equals(CONTROL_POS_SAMPLE)) {
				float time = nextFloat(tok) * secsPerTick;
				Vector3f value = nextVector3f(tok).multLocal(UNIT_SCALE);
				Keyframe k = getOrMakeKeyframe(keyframes, time);
				k.translation = value;
			}
			else if (token.charAt(0) == '}') {
				return;
			}
		}
	}

	final void scanRotTrack(StringTokenizer tok, SortedMap<Float, Keyframe> keyframes) throws Exception {
		nextOpenBrace(tok);
		while (nextToken(tok) != null) {
			if (token.equals(CONTROL_ROT_SAMPLE)) {
				float time = nextFloat(tok) * secsPerTick;
				Vector3f axis = nextVector3f(tok);
				float radians = nextFloat(tok);
				Keyframe k = getOrMakeKeyframe(keyframes, time);
				Quaternion q = new Quaternion();
				//	the first frame is baked into the TM
				if (time == 0)
					q.set(0, 0, 0, 1);
				else
					q.fromAngleAxis(-radians, axis);
				k.rotation = q;
			}
			else if (token.charAt(0) == '}') {
				//	turn relative rotations into absolutes
				getOrMakeKeyframe(keyframes, 0);
				Quaternion ori = new Quaternion(0, 0, 0, 1);
				for (Keyframe k : keyframes.values()) {
					if (k.rotation != null) {
						ori = k.rotation.mult(ori);
						ori.normalize();
						k.rotation.x = ori.x;
						k.rotation.y = ori.y;
						k.rotation.z = ori.z;
						k.rotation.w = ori.w;
					}
				}
				return;
			}
		}
	}

	final Keyframe getOrMakeKeyframe(SortedMap<Float, Keyframe> keyframes, float time) throws Exception {
		Keyframe k = keyframes.get((Float)time);
		if (k == null) {
			k = new Keyframe();
			keyframes.put((Float)time, k);
		}
		return k;
	}

	//	The transform in the file is world, should be turned local.
	//	The vertices are world, too!
	final void scanNodeTm(StringTokenizer tok, Node node) throws Exception {
		nextOpenBrace(tok);
		Matrix3f rotScale = new Matrix3f(1, 0, 0, 0, 1, 0, 0, 0, 1);
		Vector3f xlation = new Vector3f(0, 0, 0);
		while (nextToken(tok) != null) {
			if (token.charAt(0) == '}') {
				//	apply transform
				fixXlation(rotScale, xlation);
				Transform.putWorld(rotScale, xlation, node);
				return;
			}
			else if (token.charAt(0) == '{') {
				countUpDown(tok);
			}
			else if (token.equals(TM_ROW0)) {
				Vector3f v = nextVector3f(tok);
				rotScale.setColumn(0, v);
			}
			else if (token.equals(TM_ROW1)) {
				Vector3f v = nextVector3f(tok);
				rotScale.setColumn(1, v);
			}
			else if (token.equals(TM_ROW2)) {
				Vector3f v = nextVector3f(tok);
				rotScale.setColumn(2, v);
			}
			else if (token.equals(TM_ROW3)) {
				xlation = nextVector3f(tok);
				xlation.multLocal(UNIT_SCALE);
			}
		}
	}

	final void scanMesh(StringTokenizer tok, Mesh mesh) throws Exception {
		//	only scan the mesh if it's at time value 0
		boolean scanningThis = false;
		nextOpenBrace(tok);
		while (nextToken(tok) != null) {
			if (token.charAt(0) == '{') {
				countUpDown(tok);
			}
			else if (token.charAt(0) == '}') {
				meshes.add(mesh);
				return;
			}
			else if (token.equals(TIMEVALUE)) {
				scanningThis = (nextInt(tok) == 0);
			}
			else if (token.equals(MESH_VERTEX_LIST)) {
				if (scanningThis) {
					scanMeshVertexList(tok, mesh);
				}
			}
			else if (token.equals(MESH_FACE_LIST)) {
				if (scanningThis) {
					scanMeshFaceList(tok, mesh);
				}
			}
			else if (token.equals(MESH_TVERTLIST)) {
				if (scanningThis) {
					scanMeshTextureVertexList(tok, mesh);
				}
			}
			else if (token.equals(MESH_TFACELIST)) {
				if (scanningThis) {
					scanMeshTextureFaceList(tok, mesh);
				}
			}
			else if (token.equals(MESH_NORMALS)) {
				if (scanningThis) {
					scanMeshNormalList(tok, mesh);
					assert mesh.normals.logicalSize == mesh.vertexIndices.logicalSize*3;
				}
			}
		}
	}

	final void scanMeshVertexList(StringTokenizer tok, Mesh mesh) throws Exception {
		nextOpenBrace(tok);
		mesh.vertices = new SimpleFloatArray();
		while (nextToken(tok) != null) {
			if (token.charAt(0) == '}') {
				return;
			}
			else if (token.charAt(0) == '{') {
				countUpDown(tok);
			}
			else if (token.equals(MESH_VERTEX)){
				nextTokenNotNull(tok);
				Vector3f v = nextVector3f(tok);
				v.multLocal(UNIT_SCALE);
				if (MIRROR_Z) {
					v.y *= -1;
				}
				mesh.vertices.add(v);
			}
		}
	}

	final void scanMeshFaceList(StringTokenizer tok, Mesh mesh) throws Exception {
		nextOpenBrace(tok);
		mesh.vertexIndices = new SimpleIntArray();
		mesh.faceMaterials = new SimpleIntArray();
		mesh.smGroups = new SimpleIntArray();
		while (nextToken(tok) != null) {
			if (token.charAt(0) == '}') {
				return;
			}
			else if (token.charAt(0) == '{') {
				//	doesn't really happen, but forwards compatible
				countUpDown(tok);
			}
			else if (token.equals(MESH_FACE)){
				//	I'm assuming ordering: ix: A: a B: b C: c .... where I want a, b and c.
				nextTokenNotNull(tok);
				nextTokenNotNull(tok);
				int a = nextInt(tok);
				nextTokenNotNull(tok);
				int b = nextInt(tok);
				nextTokenNotNull(tok);
				int c = nextInt(tok);
				mesh.vertexIndices.add(a);
				if (!MIRROR_Z) {
					mesh.vertexIndices.add(b);
					mesh.vertexIndices.add(c);
				}
				else {
					mesh.vertexIndices.add(c);
					mesh.vertexIndices.add(b);
				}
			}
			else if (token.equals(MESH_SMOOTHING)) {
				mesh.smGroups.add(nextInt(tok));
			}
			else if (token.equals(MESH_MTLID)) {
				mesh.faceMaterials.add(nextInt(tok));
			}
		}
	}

	final void scanMeshTextureVertexList(StringTokenizer tok, Mesh mesh) throws Exception {
		nextOpenBrace(tok);
		mesh.texcoords = new SimpleFloatArray();
		while (nextToken(tok) != null) {
			if (token.charAt(0) == '}') {
				return;
			}
			else if (token.charAt(0) == '{') {
				//	doesn't really happen, but forwards compatible
				countUpDown(tok);
			}
			else if (token.equals(MESH_TVERT)){
				//	I'm assuming ordering: ix a b c .... where I want a, b and c.
				nextTokenNotNull(tok);
				mesh.texcoords.add(nextFloat(tok));
				mesh.texcoords.add(nextFloat(tok));
				nextTokenNotNull(tok);	//	I don't use the W coordinate
			}
		}
	}

	final void scanMeshTextureFaceList(StringTokenizer tok, Mesh mesh) throws Exception {
		nextOpenBrace(tok);
		mesh.textureIndices = new SimpleIntArray();
		while (nextToken(tok) != null) {
			if (token.charAt(0) == '}') {
				return;
			}
			else if (token.charAt(0) == '{') {
				//	doesn't really happen, but forwards compatible
				countUpDown(tok);
			}
			else if (token.equals(MESH_TFACE)){
				//	I'm assuming ordering: ix a b c .... where I want a, b and c.
				nextTokenNotNull(tok);
				int a = nextInt(tok);
				int b = nextInt(tok);
				int c = nextInt(tok);
				mesh.textureIndices.add(a);
				if (!MIRROR_Z) {
					mesh.textureIndices.add(b);
					mesh.textureIndices.add(c);
				}
				else {
					mesh.textureIndices.add(c);
					mesh.textureIndices.add(b);
				}
			}
		}
	}

	final void scanMeshNormalList(StringTokenizer tok, Mesh mesh) throws Exception {
		nextOpenBrace(tok);
		mesh.normals = new SimpleFloatArray();
		while (nextToken(tok) != null) {
			if (token.charAt(0) == '}') {
				return;
			}
			else if (token.charAt(0) == '{') {
				//	doesn't really happen, but forwards compatible
				countUpDown(tok);
			}
			else if (token.equals(MESH_VERTEXNORMAL)){
				//	I'm assuming ordering: ix: a b c .... where I want a, b and c.
				nextTokenNotNull(tok);
				mesh.normals.add(nextFloat(tok));
				mesh.normals.add(nextFloat(tok) * (MIRROR_Z ? -1 : 1));
				mesh.normals.add(nextFloat(tok));
			}
		}
	}

	final void countUpDown(StringTokenizer tok) throws Exception {
		int count = 1;
		while (nextToken(tok) != null) {
			if (token.charAt(0) == '}') {
				--count;
			}
			else if (token.charAt(0) == '{') {
				++count;
			}
			if (count == 0) {
				return;
			}
		}
	}

	final String nextToken(StringTokenizer tok) throws Exception {
		token = tok.hasMoreTokens() ? tok.nextToken() : null;
		if (token == null)
			return token;
		String str = null;
		int quotePos = 0;
		int quoteCount = 0;
		while ((quotePos = (token.indexOf('\"', quotePos) + 1)) > 0) {
			quoteCount++;
		}
		if (quoteCount > 0) {
			str = token;
		}
		while (((quoteCount & 1) != 0) && tok.hasMoreTokens() && ((token = tok.nextToken()) != null)) {
			quotePos = 0;
			while ((quotePos = (token.indexOf('\"', quotePos) + 1)) > 0) {
				quoteCount++;
			}
			str = str + " " + token;
		}
		token = (str == null) ? token : str;
		return token;
	}

	void nextTokenNotNull(StringTokenizer tok) throws Exception {
		String s = nextToken(tok);
		if (s == null) {
			throw new Exception("ASE file truncated before proper end.");
		}
	}

	final int nextInt(StringTokenizer tok) throws Exception {
		nextTokenNotNull(tok);
		return Integer.parseInt(token);
	}

	final void nextOpenBrace(StringTokenizer tok) throws Exception {
		nextTokenNotNull(tok);
		if (!token.equals(OPEN_BRACE)) {
			throw new Exception("Syntax error in ASE file; '{' expected.");
		}
	}

	final String nextString(StringTokenizer tok) throws Exception {
		nextTokenNotNull(tok);
		if (token.charAt(0) != '\"' || token.charAt(token.length()-1) != '\"') {
			throw new Exception("Syntax error in ASE file; proper string expected.");
		}
		return token.substring(1, token.length()-1);
	}

	final ColorRGBA nextColor(StringTokenizer tok) throws Exception {
		ColorRGBA ret = new ColorRGBA(0, 0, 0, 1);
		ret.r = nextFloat(tok);
		ret.g = nextFloat(tok);
		ret.b = nextFloat(tok);
		return ret;
	}

	final Vector3f nextVector3f(StringTokenizer tok) throws Exception {
		Vector3f ret = new Vector3f(0, 0, 0);
		ret.x = nextFloat(tok);
		ret.y = nextFloat(tok);
		ret.z = nextFloat(tok);
		return ret;
	}

	final float nextFloat(StringTokenizer tok) throws Exception {
		nextTokenNotNull(tok);
		return Float.parseFloat(token);
	}

}

